diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-30 08:28:13 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-30 08:28:13 +0000 |
| commit | 5b6313f16f508882a0ea67716b7dbaa1c6967f04 (patch) | |
| tree | 3d1d8dafea2f31274ace3fbda08333e889e06d1c /app/[lng]/engineering/(engineering)/pq_new | |
| parent | 3f0fad18483a5c800c79c5e33946d9bb384c10e2 (diff) | |
(대표님) 20250630 16시 - 유저 도메인별 라우터 분리와 보안성검토 대응
Diffstat (limited to 'app/[lng]/engineering/(engineering)/pq_new')
| -rw-r--r-- | app/[lng]/engineering/(engineering)/pq_new/[vendorId]/[submissionId]/page.tsx | 215 | ||||
| -rw-r--r-- | app/[lng]/engineering/(engineering)/pq_new/page.tsx | 96 |
2 files changed, 311 insertions, 0 deletions
diff --git a/app/[lng]/engineering/(engineering)/pq_new/[vendorId]/[submissionId]/page.tsx b/app/[lng]/engineering/(engineering)/pq_new/[vendorId]/[submissionId]/page.tsx new file mode 100644 index 00000000..28ce3128 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/pq_new/[vendorId]/[submissionId]/page.tsx @@ -0,0 +1,215 @@ +import * as React from "react" +import { Metadata } from "next" +import Link from "next/link" +import { notFound } from "next/navigation" +import { ArrowLeft } from "lucide-react" +import { Shell } from "@/components/shell" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Separator } from "@/components/ui/separator" +import { getPQById, getPQDataByVendorId } from "@/lib/pq/service" +import { unstable_noStore as noStore } from 'next/cache' +import { PQReviewWrapper } from "@/components/pq-input/pq-review-wrapper" + +export const metadata: Metadata = { + title: "PQ 검토", + description: "협력업체의 Pre-Qualification 답변을 검토합니다.", +} + +// 페이지가 기본적으로 동적임을 나타냄 +export const dynamic = "force-dynamic" + +interface PQReviewPageProps { + params: Promise<{ + vendorId: string; + submissionId: string; + }> +} + +export default async function PQReviewPage(props: PQReviewPageProps) { + // 캐시 비활성화 + noStore() + + const params = await props.params + const vendorId = parseInt(params.vendorId, 10) + const submissionId = parseInt(params.submissionId, 10) + + try { + // PQ Submission 정보 조회 + const pqSubmission = await getPQById(submissionId, vendorId) + + // PQ 데이터 조회 (질문과 답변) + const pqData = await getPQDataByVendorId(vendorId, pqSubmission.projectId || undefined) + + // 프로젝트 정보 (프로젝트 PQ인 경우) + const projectInfo = pqSubmission.projectId ? { + id: pqSubmission.projectId, + projectCode: pqSubmission.projectCode || '', + projectName: pqSubmission.projectName || '', + status: pqSubmission.status, + submittedAt: pqSubmission.submittedAt, + } : null + + // PQ 유형 및 상태 레이블 + const typeLabel = pqSubmission.type === "GENERAL" ? "일반 PQ" : "프로젝트 PQ" + const statusLabel = getStatusLabel(pqSubmission.status) + const statusVariant = getStatusVariant(pqSubmission.status) + + // 수정 가능 여부 (SUBMITTED 상태일 때만 가능) + const canReview = pqSubmission.status === "SUBMITTED" + + return ( + <Shell className="gap-6 max-w-5xl"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-4"> + <Button variant="outline" size="sm" asChild> + <Link href="/evcp/pq_new"> + <ArrowLeft className="w-4 h-4 mr-2" /> + 목록으로 + </Link> + </Button> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + {pqSubmission.vendorName} - {typeLabel} + </h2> + <div className="flex items-center gap-2 mt-1"> + <Badge variant={statusVariant}>{statusLabel}</Badge> + {projectInfo && ( + <span className="text-muted-foreground"> + {projectInfo.projectName} ({projectInfo.projectCode}) + </span> + )} + </div> + </div> + </div> + </div> + + {/* 상태별 알림 */} + {pqSubmission.status === "SUBMITTED" && ( + <Alert> + <AlertTitle>제출 완료</AlertTitle> + <AlertDescription> + 협력업체가 {formatDate(pqSubmission.submittedAt)}에 PQ를 제출했습니다. 검토 후 승인 또는 거부할 수 있습니다. + </AlertDescription> + </Alert> + )} + + {pqSubmission.status === "APPROVED" && ( + <Alert variant="success"> + <AlertTitle>승인됨</AlertTitle> + <AlertDescription> + {formatDate(pqSubmission.approvedAt)}에 승인되었습니다. + </AlertDescription> + </Alert> + )} + + {pqSubmission.status === "REJECTED" && ( + <Alert variant="destructive"> + <AlertTitle>거부됨</AlertTitle> + <AlertDescription> + {formatDate(pqSubmission.rejectedAt)}에 거부되었습니다. + {pqSubmission.rejectReason && ( + <div className="mt-2"> + <strong>사유:</strong> {pqSubmission.rejectReason} + </div> + )} + </AlertDescription> + </Alert> + )} + + <Separator /> + + {/* PQ 검토 컴포넌트 */} + <Tabs defaultValue="review" className="w-full"> + <TabsList> + <TabsTrigger value="review">PQ 검토</TabsTrigger> + <TabsTrigger value="vendor-info">협력업체 정보</TabsTrigger> + </TabsList> + + <TabsContent value="review" className="mt-4"> + <PQReviewWrapper + pqData={pqData} + vendorId={vendorId} + pqSubmission={pqSubmission} + canReview={canReview} + /> + </TabsContent> + + <TabsContent value="vendor-info" className="mt-4"> + <div className="rounded-md border p-4"> + <h3 className="text-lg font-medium mb-4">협력업체 정보</h3> + <div className="grid grid-cols-2 gap-4"> + <div> + <p className="text-sm font-medium text-muted-foreground">업체명</p> + <p>{pqSubmission.vendorName}</p> + </div> + <div> + <p className="text-sm font-medium text-muted-foreground">업체 코드</p> + <p>{pqSubmission.vendorCode}</p> + </div> + <div> + <p className="text-sm font-medium text-muted-foreground">상태</p> + <p>{pqSubmission.vendorStatus}</p> + </div> + {/* 필요시 추가 정보 표시 */} + </div> + </div> + </TabsContent> + </Tabs> + </Shell> + ) + } catch (error) { + console.error("Error loading PQ:", error) + notFound() + } +} + +// 상태 레이블 함수 +function getStatusLabel(status: string): string { + switch (status) { + case "REQUESTED": + return "요청됨"; + case "IN_PROGRESS": + return "진행 중"; + case "SUBMITTED": + return "제출됨"; + case "APPROVED": + return "승인됨"; + case "REJECTED": + return "거부됨"; + default: + return status; + } +} + +// 상태별 Badge 스타일 +function getStatusVariant(status: string): "default" | "outline" | "secondary" | "destructive" | "success" { + switch (status) { + case "REQUESTED": + return "outline"; + case "IN_PROGRESS": + return "secondary"; + case "SUBMITTED": + return "default"; + case "APPROVED": + return "success"; + case "REJECTED": + return "destructive"; + default: + return "outline"; + } +} + +// 날짜 형식화 함수 +function formatDate(date: Date | null) { + if (!date) return "날짜 없음"; + return new Date(date).toLocaleDateString("ko-KR", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit" + }); +}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/pq_new/page.tsx b/app/[lng]/engineering/(engineering)/pq_new/page.tsx new file mode 100644 index 00000000..6598349b --- /dev/null +++ b/app/[lng]/engineering/(engineering)/pq_new/page.tsx @@ -0,0 +1,96 @@ +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { searchParamsPQReviewCache } from "@/lib/pq/validations" +import { getPQSubmissions } from "@/lib/pq/service" +import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table" + +export const metadata: Metadata = { + title: "PQ 검토/실사 의뢰", + description: "", +} + +interface PQReviewPageProps { + searchParams: Promise<SearchParams> +} + +export default async function PQReviewPage(props: PQReviewPageProps) { + const searchParams = await props.searchParams + const search = searchParamsPQReviewCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 디버깅 로그 추가 + console.log("=== PQ Page Debug ==="); + console.log("Raw searchParams:", searchParams); + console.log("Raw basicFilters param:", searchParams.basicFilters); + console.log("Raw pqBasicFilters param:", searchParams.pqBasicFilters); + console.log("Parsed search:", search); + console.log("search.filters:", search.filters); + console.log("search.basicFilters:", search.basicFilters); + console.log("search.pqBasicFilters:", search.pqBasicFilters); + console.log("validFilters:", validFilters); + + // 기본 필터 처리 (통일된 이름 사용) + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + console.log("Using search.basicFilters:", basicFilters); + } else if (search.pqBasicFilters && search.pqBasicFilters.length > 0) { + // 하위 호환성을 위해 기존 이름도 지원 + basicFilters = search.pqBasicFilters + console.log("Using search.pqBasicFilters:", basicFilters); + } else { + console.log("No basic filters found"); + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + console.log("Final allFilters:", allFilters); + + // 조인 연산자도 통일된 이름 사용 + const joinOperator = search.basicJoinOperator || search.pqBasicJoinOperator || search.joinOperator || 'and'; + console.log("Final joinOperator:", joinOperator); + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getPQSubmissions({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + return ( + <Shell className="gap-4"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + PQ 검토/실사 의뢰 + </h2> + </div> + </div> + </div> + + {/* Items처럼 직접 테이블 렌더링 */} + <React.Suspense + key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링 + fallback={ + <DataTableSkeleton + columnCount={8} + searchableColumnCount={2} + filterableColumnCount={3} + cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem", "8rem", "10rem", "8rem"]} + shrinkZero + /> + } + > + <PQSubmissionsTable promises={promises} /> + </React.Suspense> + </Shell> + ) +}
\ No newline at end of file |
